创建多对多设计模式


在这里所说的主表就是定义了ManyToManyField的类就是主表

1. 使用 ManyToManyField 创建多对多设计模式

  • models.ManyToManyField(to='表的类名', related_name='反向操作使用的属性名')

  • ManyToManyField 的参数解释:

    • to -> 设置需要关联表的类名
      • to='Classes' -> 加上引号是为了在当前models.py文件中通过反射去查询表的类
      • to=Classes -> 不加引号,直接使用表的类,如果Classes类定义在to的表类下面那么就会报错,因为Classes未定义就引用了
    -> 不加引号的使用场景: 使用导入的表类时无需加引号(不会出现上面的报错情况,因为表类在文件一开始就已经导入了)
    • related_name -> 反向操作时,使用的属性名,用于代替原反向查询时的 '表名_set' 的写法 -> (通俗理解: 给主表起的别名,用于反向操作)
    • related_query_name -> 反向查询操作时,使用的连接前缀,用于替换表名
    • symmetrical -> 仅用于多对多自关联时,指定内部是否创建反向操作的字段,默认为True
    • through -> 在使用ManyToManyField字段时,Django将自动生成一张表来管理多对多的关联关系, 但我们也可以手动创建第三张表来管理多对多关系,此时就需要通过through来指定第三张表的表名
    • through_fields -> 设置关联的字段,默认使用两张表的id进行关联,through_fields=("author", "book")
    • db_table -> 设置第三张表的表名

2.创建多对多的方式一

  • 通过 ManyToManyField 自动创建第三张表
  • 正常情况下多对多的设计模式需要第三张表,如果使用了 ManyToManyField 去创建多对多设计模式,ManyToManyField 会自动帮你创建第三张表

# models.ForeignKey(to='表的类名', to_field='字段名', related_name='反向操作使用的属性名')

# 班级表
class Classes(models.Model):
    id = models.AutoField(primary_key=True)
    name = models.CharField(max_length=10)


# 学生表
class Student(models.Model):
    id = models.AutoField(primary_key=True)
    name = models.CharField(max_length=10)
    age = models.IntegerField()
    classes = models.ForeignKey(to='Classes', related_name='fkclass')


# 教师表
class Teacher(models.Model):
    id = models.AutoField(primary_key=True)
    name = models.CharField(max_length=10)
    classes = models.ManyToManyField(to='Classes')  # 通过 ManyToManyField 建立多对多关系

3.创建多对多的方式二

  • 方式二又被称之为: 中介模型

  • 设置ManyTomanyField并指定自行创建的第三张表

  • 使用该方式建立多对多关系的时候,最好将第三张表中的两个关联字段设置为联合唯一索引,因为在正常情况下多对多关系表中的两个关联字段都不可能重复

  • 该创建立方式的使用场景:
    • 当第三张表需要额外字段的时候
    • 例如: 第三张表是相亲表记录表,字段有: 男生id 女生id 相亲时间,这时就需要使用第二种方式创建

# 学生表
class Student(models.Model):
    id = models.AutoField(primary_key=True)
    name = models.CharField(max_length=10)
    age = models.IntegerField()
    classes = models.ForeignKey(to='Classes', related_name='fkclass')


# 班级表
class Classes(models.Model):
    id = models.AutoField(primary_key=True)
    name = models.CharField(max_length=10)


# 教师表
class Teacher(models.Model):
    id = models.AutoField(primary_key=True)
    name = models.CharField(max_length=10)
    classes = models.ManyToManyField(to='Classes', through='TeacherToClasses', through_fields=('teacher', 'classes'))
'''
        through: 指定第三张表的类名
        through_fields: 第一个参数的字段名一定是第三张表中与主表(定义了ManyToManyField的类就是主表)相关的字段名,否则会报错
    '''


# 教师和班级的多对多关联表
class TeacherToClasses(models.Model):
    id = models.AutoField(primary_key=True)
    teacher = models.ForeignKey(to='Teacher')
    classes = models.ForeignKey(to='Classes')

    class Meta:
# 建立联合唯一索引
        unique_together = ("teacher", "classes")

  • 注意: 

    • 使用该方式所建立的多对多关系,ManyToManyField 中的所有方法都会失效(如: add()/create()/set()/clear()/remove()),所以只能直接对第三张表进行数据的增删改操作

    • 使用该方式所建立的多对多关系,正向查询 和 反向查询 可以正常使用

# 因为 set() 等方法都无效,只能直接对第三张表进行数据的增删改操作

TeacherToClasses.objects.create(classes_id=1, teacher_id=1)

4.创建多对多的方式三: 

  • 自行创建第三张表 -> 不推荐使用

  • 使用该方式所建立的多对多关系,ManyToManyField 中的所有方法都会失效(如: add()/create()/set()/clear()/remove()),所以只能直接对第三张表进行数据的增删改操作

  • 使用该方式所建立的多对多关系,正向查询 和 反向查询 都无法使用

  • 使用该方式建立多对多关系的时候,最好将第三张表中的两个关联字段设置为联合唯一索引,因为在正常情况下多对多关系表中的两个关联字段都不可能重复

  • 注意: 如果使用了自行创建第三张表的方式,那么在ORM层面作者和书就没有多对多的关系了,就无法直接通过作者找到与他有关的书了

# 学生表
class Student(models.Model):
    id = models.AutoField(primary_key=True)
    name = models.CharField(max_length=10)
    age = models.IntegerField()
    classes = models.ForeignKey(to='Classes', related_name='fkclass')


# 班级表
class Classes(models.Model):
    id = models.AutoField(primary_key=True)
    name = models.CharField(max_length=10)


# 教师表
class Teacher(models.Model):
    id = models.AutoField(primary_key=True)
    name = models.CharField(max_length=10)


# 教师和班级的多对多关联表
class TeacherToClasses(models.Model):
    id = models.AutoField(primary_key=True)
    teacher = models.ForeignKey(to='Teacher')
    classes = models.ForeignKey(to='Classes')

    class Meta:
# 建立联合唯一索引
        unique_together = ("teacher", "classes")

  • 例子: 一个老师可以带多个班级,一个班级可以有多个老师

ManyToManyField字段相关


  • ManyToManyField字段相关的和外键字段相关的是一样的(不懂的看回外键章节中的外键字段相关的)

相关方法


1. .create()

  • 创建一个新的对象,保存新对象,并将新对象添加到所要关联对象集之中,返回新创建的对象

  • 语法: 查询到的对象.类的ManyToManyField属性名.create(字段名='xxx')

# 例子: 通过老师对象添加一个新的班级,并且与该老师进行关联 -> 通俗理解: 添加一个新的班级,并且把该班级与查询到的老师进行关

teacher = Teacher.objects.get(id=1)
classes_obj = teacher.classes.create(name='七班')
print(classes_obj)  # Classes object

2. .add()

  • 把查询到的对象或id,添加到与之关联的对象集中 -> 通俗理解: 往多对多的表中添加数据的方法

  • 添加对象的写法

    • 语法: 查询到的对象.类的ManyToManyField属性名.add(对象, 对象, ……)

# 例子: 将多个班级与指定的老师建立关联

class_lis = Classes.objects.filter(id__lt=3)
Teacher.objects.get(id=1).classes.add(*class_lis)

  • 添加id的写法

    • 语法: 查询到的对象.类的ManyToManyField属性名.add(id, id, ……)

# 例子: 将多个班级与指定的老师建立关联

Teacher.objects.get(id=1).classes.add(*[1, 2])

# 等同于

Teacher.objects.get(id=1).classes.add(1, 2)

3. .set()

  • 修改多对多的表中数据的方法

  • 注意: 
    • 在使用完 .set() 方法后,一定要执行 .save()
    • .set() 方法自带了 .add() 方法的功能,所以可以把 .set() 当做 .add() 使用,但是所要传递的参数是有区别的
    • .set() 方法会先清除原数据,然后再添加新的数据

  • 添加对象的写法

    • 语法: 查询到的对象.类的ManyToManyField属性名.set([对象, 对象, ……]) -> 接收一个可迭代对象

# 例子: 修改id为1的教师,所带的班级id为 (1, 3, 5) -> 原来带的班级是(6, 7, 8)

class_list = Classes.objects.filter(id__in=[1, 3, 5])
teacher = Teacher.objects.get(id=1)
teacher.classes.set(class_list)
teacher.save()

  • 添加id的写法

    • 语法: 查询到的对象.类的ManyToManyField属性名.set([1, 2, 3]) -> 接收一个可迭代对象

# 例子: 修改id为1的教师,所带的班级id为 (1, 3, 5) -> 原来带的班级是(6, 7, 8)

teacher = Teacher.objects.get(id=1)
teacher.classes.set([1, 3, 5])
teacher.save()

4. .remove()

  • 删除多对多的表中数据的方法

  • 所传入的参数是对象的写法

    • 语法: 查询到的对象.类的ManyToManyField属性名.remove(对象, 对象, ……)

# 删除多对多表中 teacher_id=1 且 classes_id=2 和 classes_id=4 的数据

class_list = Classes.objects.filter(id__in=[2, 4])
Teacher.objects.get(id=1).classes.remove(*class_list)

  • 所传入的参数是id的写法

    • 语法: 查询到的对象.类的ManyToManyField属性名.remove(1, 2, 3)

# 删除多对多表中 teacher_id=1 且 classes_id=2 和 classes_id=4 的数据

Teacher.objects.get(id=1).classes.remove(2, 4)

5. .clear()

  • 清空指定对象在多对多表中的所有数据

  • 语法: 查询到的对象.类的ManyToManyField属性名.clear()

Teacher.objects.get(id=1).classes.clear()

6. .remove() 和 .clear() 的注意事项

  • 对于ForeignKey对象,当 ForeignKey 设置为 null=True 时 clear()和remove()方法才能使用

  • 当  ForeignKey 字段没设置 null=True 时

# models.py

class Student(models.Model):
    id = models.AutoField(primary_key=True)
    name = models.CharField(max_length=10)
    classes = models.ForeignKey(to='Classes')

# views.py

classes_obj.student_set.clear()

# 报错提示

Traceback (most recent call last):
  File "C:/Users/Mr. Yeung/Desktop/PyFolder/SMS/test_orm.py", line 12, in <module>
    classes_obj.student_set.clear()
AttributeError: 'RelatedManager' object has no attribute 'clear'

  • 当  ForeignKey 字段设置了 null=True 时

# models.py

class Student(models.Model):
    id = models.AutoField(primary_key=True)
    name = models.CharField(max_length=10)
    classes = models.ForeignKey(to='Classes', null=True)

# views.py

classes_obj.student_set.clear()

查询


1. 正向查询

  • 正向查询用字段,反向查询用表名

  • 写法一:

    • 通过对象查找
    • 语法: 查询到的对象.类的ManyToManyField属性名.all/filter/get() -> 返回值:QuerySet

classes_list = Teacher.objects.get(id=1).classes.all()  # <QuerySet [<Classes: Classes object>, <Classes: Classes object>, <Classes: Classes object>]>

for classes in classes_list:
    print(classes.id, classes.name)  # 1 一班

  • 写法二:

    • 使用 values 或 values_list 通过字段查询

    • 语法:

      • .values('主表字段名', '主表字段名', '类的ManyToManyField属性名__从表字段名', '类的ManyToManyField属性名__类的ManyToManyField属性名下的ManyToManyField属性名__从表字段名')

      • .values_list('主表字段名', '主表字段名', '类的ManyToManyField属性名__从表字段名', '类的ManyToManyField属性名__类的ManyToManyField属性名下的ManyToManyField属性名__从表字段名')

data1 = Teacher.objects.values('name', 'classes__id', 'classes__name')
print(data1)  # <QuerySet [{'classes__name': '一班', 'name': '李老师', 'classes__id': 1}, {'classes__name': '二班', 'name': '李老师', 'classes__id': 2}, ……]>

data2 = Teacher.objects.values_list('name', 'classes__id', 'classes__name')
print(data2)  # <QuerySet [('李老师', 1, '一班'), ('李老师', 2, '二班'), ('李老师', 3, '三班'), ('黄老师', 2, '二班'), ('黄老师', 3, '三班'), ('杨老师', 1, '一班')]>

    • 跨多张表进行查询 ->查找书名是“西游记”的书的作者的年龄和手机号码和作者详情id(是ORM练习中的最后一题)

author_list = Book.objects.filter(title='西游记').values('author__name', 'author__detail_id', 'author__detail__addr', 'author__detail__email')

2. 反向查询

  • 正向查询用字段,反向查询用表名

  • 写法一:

    • 通过对象查找
    • 语法: 查询到的对象.主表的类名_set.all/get/filter() -> 主表类名首字母无需大写

# 例子: 查询一班的所有学生

teacher_list = Classes.objects.get(id=1).teacher_set.all()  # <QuerySet [<Teacher: Teacher object>, <Teacher: Teacher object>]>

for teacher in teacher_list:
    print(teacher.id, teacher.name)  # 1 李老师

    • 设置了 related_name='xxx'

# models.py

class Teacher(models.Model):
    id = models.AutoField(primary_key=True)
    name = models.CharField(max_length=10)
    classes = models.ManyToManyField(to='Classes', related_name='mfclass')

# views.py

teacher_list = Classes.objects.get(id=1).mfclass.all()  # <QuerySet [<Teacher: Teacher object>, <Teacher: Teacher object>]>

for teacher in teacher_list:
    print(teacher.id, teacher.name)  # 1 李老师

  • 写法二:

    • 使用 values 或 values_list 通过字段查询

    • 语法:

      • 如果没有设置表类中外键属性的 related_name

        • .values('从表字段名', '从表字段名', '主表类名__主表字段名', '主表类名__主表字段名') -> 主表类名无需大写
        • .values_list('从表字段名', '从表字段名', '主表类名__主表字段名', '主表类名__主表字段名') -> 主表类名无需大写

teacher_list1 = Classes.objects.all().values('name', 'teacher__id', 'teacher__name')
print(teacher_list1)  # < QuerySet[{'teacher__name': '李老师', 'teacher__id': 1, 'name': '一班'}, {'teacher__name': '李老师', 'teacher__id': 1, 'name': '二班'}, ……] >

teacher_list2 = Classes.objects.all().values_list('name', 'teacher__id', 'teacher__name')
print(teacher_list2)  # < QuerySet[('一班', 1, '李老师'), ('二班', 1, '李老师'), ('三班', 1, '李老师'), ('二班', 2, '黄老师'), ('三班', 2, '黄老师'), ('一班', 3, '杨老师'), ('四班', None, None)] >

      • 如果设置了表类中外键属性的 related_name

        • .values('从表字段名', '从表字段名', 'related_name所设置的名字__主表字段名', 'related_name所设置的名字__主表字段名')
        • .values_list('从表字段名', '从表字段名', 'related_name所设置的名字__主表字段名', 'related_name所设置的名字__主表字段名')

# models.py

class Teacher(models.Model):
    id = models.AutoField(primary_key=True)
    name = models.CharField(max_length=10)
    classes = models.ManyToManyField(to='Classes', related_name='mfclass')

# views.py

teacher_list1 = Classes.objects.all().values('name', 'mfclass__id', 'mfclass__name')
print(teacher_list1)  # <QuerySet [{'mfclass__name': '李老师', 'mfclass__id': 1, 'name': '一班'}, {'mfclass__name': '李老师', 'mfclass__id': 1, 'name': '二班'}, ……]>

teacher_list2 = Classes.objects.all().values_list('name', 'mfclass__id', 'mfclass__name')
print(teacher_list2)  # <QuerySet [('一班', 1, '李老师'), ('二班', 1, '李老师'), ('三班', 1, '李老师'), ('二班', 2, '黄老师'), ('三班', 2, '黄老师'), ('一班', 3, '杨老师'), ('四班', None, None)]>

删除数据的注意事项


1. 如果Teacher表和Classes表创建了多对多关系后生成了第三张表,当Teacher表中有一条数据被删除了,那么多对多关系表中与该条数据相关的所有数据都会被删除